﻿using System;

using GL = MonoMac.OpenGL.GL;
using OpenGL = MonoMac.OpenGL;

using Storyteller.Framework.Math;

namespace Storyteller.Framework.Graphics
{
	public class SpriteBatch
	{
		public SpriteBatch(SpriteShader defaultShader)
			: this(1000, defaultShader)
		{
		}

		public SpriteBatch(int maxSprites, SpriteShader defaultShader)
		{
			this.defaultShader = defaultShader;

			vertices = new Vertex[maxSprites * 6];
			maxVertices = maxSprites * 6;
		}

		public void Begin()
		{
			Begin(BlendMode.Alpha, defaultShader, Matrix4.Identity);
		}

		public void Begin(BlendMode blendState)
		{
			Begin(blendMode, defaultShader, Matrix4.Identity);
		}

		public void Begin(BlendMode blendMode, Matrix4 batchTransform)
		{
			Begin(blendMode, defaultShader, batchTransform);
		}

		public void Begin(BlendMode blendMode, SpriteShader spriteShader)
		{
			Begin(blendMode, spriteShader, Matrix4.Identity);
		}

		public void Begin(BlendMode blendMode, SpriteShader spriteShader, Matrix4 batchTransform)
		{
			if (insideBegin)
				throw new InvalidOperationException("You must call SpriteBatch.End before calling SpriteBatch.Begin again");

			insideBegin = true;

			this.blendMode = blendMode;
			this.batchTransform = batchTransform;
			currentShader = spriteShader;
			currentTexture = null;
			activeVertices = 0;
		}

		public void End()
		{
			if (!insideBegin)
				throw new InvalidOperationException("SpriteBatch.End called without SpriteBatch.Begin");

			insideBegin = false;

			if (activeVertices >= 6)
				DrawBuffer();

			currentTexture = null;
			activeVertices = 0;
		}

		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color)
		{
			Draw(texture, position, textureRegion.Bounds, color, 0, textureRegion.OriginCenter, Vector2.One, textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None, Matrix4.Identity);
		}

		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			Draw(texture, position, textureRegion.Bounds, color, rotation, textureRegion.OriginCenter, scale, effects | (textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None), transformMatrix);
		}

		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			origin.X = MathHelper.Lerp(textureRegion.OriginTopLeft.X, textureRegion.OriginBottomRight.X, origin.X);
			origin.Y = MathHelper.Lerp(textureRegion.OriginTopLeft.Y, textureRegion.OriginBottomRight.Y, origin.Y);
			Draw(texture, position, textureRegion.Bounds, color, rotation, origin, scale, effects | (textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None), transformMatrix);
		}

		public void Draw(Texture texture, Vector2 position, Color color)
		{
			Draw(texture, position, null, color, 0, Vector2.Zero, Vector2.One, TextureEffects.None, Matrix4.Identity);
		}

		public void Draw(Texture texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			if (activeVertices == maxVertices || (currentTexture != null && !currentTexture.Equals(texture)))
				DrawBuffer();

			if(texture is RenderTexture)
				effects |= TextureEffects.FlipVertically;

			currentTexture = texture;

			position -= new Vector2(0.5f);

			Rectangle textureRect = new Rectangle(0, 0, texture.Width, texture.Height);
			Rectangle rectangle;

			if (sourceRectangle.HasValue)
				rectangle = sourceRectangle.Value;
			else
				rectangle = textureRect;

			int width = rectangle.Width;
			int height = rectangle.Height;

			int textureWidth = texture.Width;
			int textureHeight = texture.Height;

			Vector2 uvTL, uvTR, uvBL, uvBR;
			Vector2 temp;

			if ((effects & TextureEffects.RotatePackedUVs) != TextureEffects.None)
			{
				uvTL = new Vector2((float)(rectangle.X + rectangle.Height) / textureWidth,
					(float)rectangle.Y / textureHeight);
				uvTR = new Vector2((float)(rectangle.X + rectangle.Height) / textureWidth,
					(float)(rectangle.Y + rectangle.Width) / textureHeight);
				uvBL = new Vector2((float)rectangle.X / textureWidth,
					(float)rectangle.Y / textureHeight);
				uvBR = new Vector2((float)rectangle.X / textureWidth,
					(float)(rectangle.Y + rectangle.Width) / textureHeight);
			}
			else
			{
				uvTL = new Vector2(rectangle.X / (float)textureWidth, rectangle.Y / (float)textureHeight);
				uvTR = new Vector2((rectangle.X + rectangle.Width) / (float)textureWidth, rectangle.Y / (float)textureHeight);
				uvBL = new Vector2(rectangle.X / (float)textureWidth, (rectangle.Y + rectangle.Height) / (float)textureHeight);
				uvBR = new Vector2((rectangle.X + rectangle.Width) / (float)textureWidth, (rectangle.Y + rectangle.Height) / (float)textureHeight);
			}

			if ((effects & TextureEffects.RotateClockwise90) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvBL;
				uvBL = uvBR;
				uvBR = uvTR;
				uvTR = temp;
			}
			if ((effects & TextureEffects.FlipVertically) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvBL;
				uvBL = temp;
				temp = uvTR;
				uvTR = uvBR;
				uvBR = temp;
			}
			if ((effects & TextureEffects.FlipHorizontally) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvTR;
				uvTR = temp;
				temp = uvBL;
				uvBL = uvBR;
				uvBR = temp;
			}

			Vector3 v;
			Matrix4 transform = Matrix4.Scale(scale.X, scale.Y, 1.0f) *
				Matrix4.CreateRotationZ(rotation) *
				Matrix4.CreateTranslation(position.X, position.Y, 0) *
				transformMatrix * batchTransform;

			v.X = -width * origin.X;
			v.Y = -height * origin.Y;
			v.Z = 0;
			v = Vector3.Transform(v, transform);

			vertices[activeVertices].x = v.X;
			vertices[activeVertices].y = v.Y;
			vertices[activeVertices].r = color.R;
			vertices[activeVertices].g = color.G;
			vertices[activeVertices].b = color.B;
			vertices[activeVertices].a = color.A;
			vertices[activeVertices].u = uvTL.X;
			vertices[activeVertices++].v = uvTL.Y;

			v.X = width * (1 - origin.X);
			v.Y = -height * origin.Y;
			v.Z = 0;
			v = Vector3.Transform(v, transform);

			vertices[activeVertices].x = v.X;
			vertices[activeVertices].y = v.Y;
			vertices[activeVertices].r = color.R;
			vertices[activeVertices].g = color.G;
			vertices[activeVertices].b = color.B;
			vertices[activeVertices].a = color.A;
			vertices[activeVertices].u = uvTR.X;
			vertices[activeVertices++].v = uvTR.Y;

			v.X = -width * origin.X;
			v.Y = height * (1 - origin.Y);
			v.Z = 0;
			v = Vector3.Transform(v, transform);

			vertices[activeVertices].x = v.X;
			vertices[activeVertices].y = v.Y;
			vertices[activeVertices].r = color.R;
			vertices[activeVertices].g = color.G;
			vertices[activeVertices].b = color.B;
			vertices[activeVertices].a = color.A;
			vertices[activeVertices].u = uvBL.X;
			vertices[activeVertices++].v = uvBL.Y;

			vertices[activeVertices] = vertices[activeVertices - 1];
			activeVertices++;
			vertices[activeVertices] = vertices[activeVertices - 3];
			activeVertices++;

			v.X = width * (1 - origin.X);
			v.Y = height * (1 - origin.Y);
			v.Z = 0;
			v = Vector3.Transform(v, transform);

			vertices[activeVertices].x = v.X;
			vertices[activeVertices].y = v.Y;
			vertices[activeVertices].r = color.R;
			vertices[activeVertices].g = color.G;
			vertices[activeVertices].b = color.B;
			vertices[activeVertices].a = color.A;
			vertices[activeVertices].u = uvBR.X;
			vertices[activeVertices++].v = uvBR.Y;
		}

		public void DrawString(Font font, string s, Vector2 position, Color color, float rotation, Vector2 scale, Matrix4 transformMatrix)
		{
			Texture texture = font.Texture;

			if (activeVertices == maxVertices || (currentTexture != null && !currentTexture.Equals(texture)))
				DrawBuffer();

			currentTexture = texture;

			Vector2 origin = Vector2.Zero;

			float x = 0, y = 0;

			for (int i = 0; i < s.Length; i++)
			{
				CharacterData cd = font[(short)s[i]];

				if (cd.Width == 0 || cd.Height == 0)
				{
					x += cd.AdvanceX;
					continue;
				}

				if (activeVertices == maxVertices)
					DrawBuffer();

				Rectangle sourceRectangle = cd.TextureRegion.Bounds;

				int width = sourceRectangle.Width;
				int height = sourceRectangle.Height;

				int textureWidth = texture.Width;
				int textureHeight = texture.Height;

				Vector2 uvTL;
				Vector2 uvTR;
				Vector2 uvBL;
				Vector2 uvBR;

				if (cd.TextureRegion.Rotated)
				{
					uvTL = new Vector2((float)(sourceRectangle.X + sourceRectangle.Height) / textureWidth,
						(float)sourceRectangle.Y / textureHeight);
					uvTR = new Vector2((float)(sourceRectangle.X + sourceRectangle.Height) / textureWidth,
						(float)(sourceRectangle.Y + sourceRectangle.Width) / textureHeight);
					uvBL = new Vector2((float)sourceRectangle.X / textureWidth,
						(float)sourceRectangle.Y / textureHeight);
					uvBR = new Vector2((float)sourceRectangle.X / textureWidth,
						(float)(sourceRectangle.Y + sourceRectangle.Width) / textureHeight);
				}
				else
				{
					uvTL = new Vector2(sourceRectangle.X / (float)textureWidth, sourceRectangle.Y / (float)textureHeight);
					uvTR = new Vector2((sourceRectangle.X + sourceRectangle.Width) / (float)textureWidth, sourceRectangle.Y / (float)textureHeight);
					uvBL = new Vector2(sourceRectangle.X / (float)textureWidth, (sourceRectangle.Y + sourceRectangle.Height) / (float)textureHeight);
					uvBR = new Vector2((sourceRectangle.X + sourceRectangle.Width) / (float)textureWidth, (sourceRectangle.Y + sourceRectangle.Height) / (float)textureHeight);
				}

				Vector3 v;
				Matrix4 transform = Matrix4.Scale(scale.X, scale.Y, 1) *
					Matrix4.CreateRotationZ(rotation) *
					Matrix4.CreateTranslation(position.X + x + cd.OffsetX, position.Y + y - cd.OffsetY, 0) *
					transformMatrix * batchTransform;

				v.X = -width * origin.X;
				v.Y = -height * origin.Y;
				v.Z = 0;
				v = Vector3.Transform(v, transform);

				vertices[activeVertices].x = v.X;
				vertices[activeVertices].y = v.Y;
				vertices[activeVertices].r = color.R;
				vertices[activeVertices].g = color.G;
				vertices[activeVertices].b = color.B;
				vertices[activeVertices].a = color.A;
				vertices[activeVertices].u = uvTL.X;
				vertices[activeVertices++].v = uvTL.Y;

				v.X = width * (1 - origin.X);
				v.Y = -height * origin.Y;
				v.Z = 0;
				v = Vector3.Transform(v, transform);

				vertices[activeVertices].x = v.X;
				vertices[activeVertices].y = v.Y;
				vertices[activeVertices].r = color.R;
				vertices[activeVertices].g = color.G;
				vertices[activeVertices].b = color.B;
				vertices[activeVertices].a = color.A;
				vertices[activeVertices].u = uvTR.X;
				vertices[activeVertices++].v = uvTR.Y;

				v.X = -width * origin.X;
				v.Y = height * (1 - origin.Y);
				v.Z = 0;
				v = Vector3.Transform(v, transform);

				vertices[activeVertices].x = v.X;
				vertices[activeVertices].y = v.Y;
				vertices[activeVertices].r = color.R;
				vertices[activeVertices].g = color.G;
				vertices[activeVertices].b = color.B;
				vertices[activeVertices].a = color.A;
				vertices[activeVertices].u = uvBL.X;
				vertices[activeVertices++].v = uvBL.Y;

				vertices[activeVertices] = vertices[activeVertices - 1];
				activeVertices++;
				vertices[activeVertices] = vertices[activeVertices - 3];
				activeVertices++;

				v.X = width * (1 - origin.X);
				v.Y = height * (1 - origin.Y);
				v.Z = 0;
				v = Vector3.Transform(v, transform);

				vertices[activeVertices].x = v.X;
				vertices[activeVertices].y = v.Y;
				vertices[activeVertices].r = color.R;
				vertices[activeVertices].g = color.G;
				vertices[activeVertices].b = color.B;
				vertices[activeVertices].a = color.A;
				vertices[activeVertices].u = uvBR.X;
				vertices[activeVertices++].v = uvBR.Y;

				x += cd.AdvanceX;
			}
		}
		
		void DrawBuffer()
		{
			GL.GetInteger(OpenGL.GetPName.Viewport, viewport);
			currentShader.SetResolution(viewport[2], viewport[3]);

			GL.Enable(OpenGL.EnableCap.Blend);

			switch (blendMode)
			{
				case BlendMode.None:
					GL.BlendEquationSeparate(OpenGL.BlendEquationMode.FuncAdd, OpenGL.BlendEquationMode.FuncAdd);
					GL.BlendFuncSeparate(OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.Zero, OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.Zero);
					break;
				case BlendMode.Alpha:
					GL.BlendEquationSeparate(OpenGL.BlendEquationMode.FuncAdd, OpenGL.BlendEquationMode.FuncAdd);
					GL.BlendFuncSeparate(OpenGL.BlendingFactorSrc.SrcAlpha, OpenGL.BlendingFactorDest.OneMinusSrcAlpha, OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.Zero);
					break;
				case BlendMode.Additive:
					GL.BlendEquationSeparate(OpenGL.BlendEquationMode.FuncAdd, OpenGL.BlendEquationMode.FuncAdd);
					GL.BlendFuncSeparate(OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.One, OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.Zero);
					break;
				case BlendMode.PremultipliedAlpha:
					GL.BlendEquationSeparate(OpenGL.BlendEquationMode.FuncAdd, OpenGL.BlendEquationMode.FuncAdd);
					GL.BlendFuncSeparate(OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.OneMinusSrcAlpha, OpenGL.BlendingFactorSrc.One, OpenGL.BlendingFactorDest.Zero);
					break;
			}

			currentShader.Draw(vertices, activeVertices, currentTexture.TextureId);
			activeVertices = 0;
		}

		SpriteShader defaultShader;

		bool insideBegin = false;

		Texture currentTexture = null;
		SpriteShader currentShader = null;

		BlendMode blendMode = BlendMode.Alpha;

		int[] viewport = new int[4];

		Vertex[] vertices;
		int maxVertices;
		int activeVertices = 0;

		Matrix4 batchTransform = Matrix4.Identity;
	}
}
